Skip to content

fix(typescript): generate non-nullable json columns as NonNullable<Json> - #1085

Closed
Maliik-B wants to merge 1 commit into
supabase:masterfrom
Maliik-B:fix/jsonb-not-null-nullability
Closed

fix(typescript): generate non-nullable json columns as NonNullable<Json>#1085
Maliik-B wants to merge 1 commit into
supabase:masterfrom
Maliik-B:fix/jsonb-not-null-nullability

Conversation

@Maliik-B

Copy link
Copy Markdown

Problem

A NOT NULL json/jsonb column generates the TypeScript type Json, but the generated Json type is itself nullable:

export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]

So the column type structurally permits null even though Postgres forbids it. Studio reports the column as non-nullable, but the generated types disagree.

Closes #1055.

Fix

generateNullableUnionTsType already special-cases unknown/any (which include null) by not appending | null. This applies the same reasoning to Json: a non-nullable json/jsonb column is narrowed to NonNullable<Json> so the type reflects the database constraint.

if (tsType === 'Json' && !isNullable) {
  return `NonNullable<${tsType}>`
}

Scope

  • Only non-nullable json/jsonb columns change (Json becomes NonNullable<Json>). Nullable json columns are untouched, since Json already covers null.
  • The change sits in the single nullability chokepoint, so it covers Row, Insert, and Update consistently. Function return types and composite-type attributes call this helper with isNullable: true, so they are unaffected. Array types ((Json)[]) are unaffected.

Tests

Added a regression test that spins up a throwaway schema with a NOT NULL jsonb column and a nullable jsonb column, generates types scoped to it, and asserts the non-nullable column is NonNullable<Json> while the nullable one stays Json | null. It follows the inline-schema pattern from the existing included/excluded-schemas test, so it adds no fixture churn to the shared snapshots.

Note on the type-generation extraction

I see #1084 and supabase/pg-toolbelt#302 are moving these templates into @supabase/postgrest-typegen. This patch targets typescript.ts here; happy to port the same one-line guard to the extracted package instead if that is the better home. @avallete

The generated Json type includes null, so a NOT NULL json/jsonb column was typed as nullable. Narrow it to NonNullable<Json> in the nullability helper so the type reflects the database constraint. Nullable json columns are unchanged since Json already covers null.

Closes supabase#1055
@spydon

spydon commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Thank you for the contribution! postgres-meta's type generation is moving to the shared @supabase/postgrest-typegen package in supabase/sdk (see #1084), so open template fixes are being re-landed there. Your fix (narrowing non-nullable json columns to NonNullable<Json>) has been ported in supabase/sdk#125 with credit to this PR.

@spydon spydon closed this Aug 31, 2026
spydon added a commit to supabase/sdk that referenced this pull request Aug 31, 2026
… Args, trigger-writable views (#125)

## Summary

Ports the worthwhile TypeScript generator fixes from postgres-meta's
open template PRs into this package (the templates are being deleted in
favor of this package in supabase/postgres-meta#1084, so open fixes
there are triaged and re-landed here). Four fixes, one commit each:

1. **Stored generated columns omitted from Insert/Update** (from
supabase/postgres-meta#1105): `GENERATED ALWAYS AS ... STORED` columns
reject writes in Postgres, but only identity-ALWAYS columns were
excluded; both now emit `?: never`.
2. **Non-nullable json narrowed to `NonNullable<Json>`** (from
supabase/postgres-meta#1085): the emitted `Json` type includes `null`,
so a NOT NULL json/jsonb column structurally permitted null. Known
accepted edge: a NOT NULL jsonb column holding a JSON `'null'::jsonb`
value still serializes as JS `null`, so Row is optimistic in that case;
Insert/Update narrowing is fully sound.
3. **Zero-argument function Args typed `Record<PropertyKey, never>`**
(the still-valid half of supabase/postgres-meta#1035): `Args: never`
makes postgrest-js treat every zero-argument function as a computed
field (`never extends { '': Row }` always holds), dropping same-named
columns from `select('*')` results, and an uninhabited `Database` breaks
sound type tooling. Verified against postgrest-js, whose
`IsMatchingArgs` special-cases `Record<PropertyKey, never>`.
4. **Insert/Update types for INSTEAD OF trigger views** (from
supabase/postgres-meta#1062, reimplemented): views made writable by
INSTEAD OF triggers got no Insert/Update types. Views now carry
`is_insert_enabled`/`is_update_enabled` computed via
`pg_relation_is_updatable(oid, true)` (bit 8 INSERT, bit 4 UPDATE; also
covers INSTEAD rules), gated independently, and column updatability
counts triggers too (`pg_column_is_updatable(oid, attnum, true)` plus an
explicit INSTEAD OF INSERT trigger check, since that function only
considers the UPDATE event). The origin PR duplicated hand-rolled
pg_trigger subqueries with one pair of wrong bit values and left
trigger-writable columns degrading to `?: never`, visible in its own
snapshot. The two new `PostgresView` fields are additive (metadata
version stays 1), documented, and mirrored in the frozen equivalence
contract.

## Triage of origin PRs

| postgres-meta PR | Verdict | Reasoning |
|---|---|---|
| #1105 | Ported | Two-line correctness fix; `is_generated` was already
introspected. |
| #1085 | Ported | Nullability chokepoint fix; function returns and
composite attributes untouched. |
| #1035 | Ported (zero-arg half) | The computed-field-filtering half is
superseded: this package introspects with `includeTableTypes: true`, so
table/view row types already resolve (parity golden shows computed
fields working). Only foreign-table row types remain uncovered; the PR's
name-string matching is too fragile to port for that niche. |
| #1062 | Reimplemented | Right idea, broken execution (wrong tgtype
bits in one duplicated subquery pair, all-`never` Update output in its
own snapshot). |
| #1063 (TS part) | Skipped | Superseded: composite attributes already
emit `| null` on main; the PR's remaining delta (`unknown | null`) is
the identical type. |
| #1048 (vector to `number[]`) | Skipped | Wrong as a global remap:
PostgREST serializes pgvector as strings in responses, so Row types
would regress; the reviewer asked for e2e evidence and got none. Needs
input/output-aware mapping, a design discussion. |
| #973 (`| string` numeric inserts) | Skipped | Maintainer requested
changes: breaking for consumers expecting `number`; per-column overrides
are the escape hatch. |
| #573 | Skipped | Blanket `| null` on function args/returns is breaking
(author concedes); the centralization half is superseded by the current
generator; the domain-resolution gap is real but needs a metadata
contract extension (feature-scale, raised separately). |
| #750 (`Json` to `unknown`) | Skipped | Breaking; major-version
decision. |
| #1044 (int8 to `bigint`) | Skipped | Breaking, and incorrect without a
custom JSON parser. |
| #1083 (`bigint_as` option) | Skipped | Feature/option with API design
questions, not a fix. |
| #814 (json_schema constraint types) | Skipped | New feature. |

## Validation

- Unit tests per fix, plus Docker-backed introspection integration tests
proving a join view with an INSTEAD OF INSERT trigger introspects as
insert-enabled/update-disabled with updatable columns, and
auto-updatable views keep both flags.
- Parity golden regenerated and reviewed line by line: the only change
is 14 zero-argument functions switching `Args: never` to `Args:
Record<PropertyKey, never>`. Fixes 1, 2 and 4 have no fixture-visible
effect.
- `check-types`, `format-and-lint`, `knip`, `build`, `test` (99 pass
across 12 files) all green.
- Note: the nightly parity job against real postgres-meta will show this
intentional drift until postgres-meta consumes a release containing it
(supabase/postgres-meta#1084 replaces the templates with this package,
closing the gap).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated type for JSONB NOT NULL column allows null

2 participants